package com.bertie.smiles2iupac;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

@SuppressWarnings("serial")
public class Iupac {

	private static String[] IUPAC_NAMES = { "", "Meth", "Eth", "Prop", "But", "Pent", "Hex", "Hept", "Oct", "Non", "Dec", "Undec", "Dodec", "Tridec", "Tetradec", "Pentadec", "Hexadec", "Heptadec",
			"Octadec", "Nonadec", "Eicos", "Heneicos", "Docos", "Tricos", "Tetracos", "Pentacos", "Hexacos", "Heptacos", "Octacos", "Nonacos", "Triacont", "Hentriacont", "Dotriacont", "Tritriacont",
			"Tetratriacont", "Pentatriacont", "Hexatriacont", "Heptatriacont", "Octatriacont", "Nonatriacont", "Tetracont", "Hentetracont", "Dotetracont", "Tritetracont", "Tetratetracont",
			"Pentatetracont", "Hexatetracont", "Heptatetracont", "Octatetracont", "Nonatetracont", "Pentacont", "Henpentacont", "Dopentacont", "Tripentacont", "Tetrapentacont", "Pentapentacont",
			"Hexapentacont", "Heptapentacont", "Octapentacont", "Nonapentacont", "Hexacont", "Henhexacont", "Dohexacont", "Trihexacont", "Tetrahexacont", "Pentahexacont", "Hexahexacont",
			"Heptahexacont", "Octahexacont", "Nonahexacont", "Heptacont", "Henheptacont", "Doheptacont", "Triheptacont", "Tetraheptacont", "Pentaheptacont", "Hexaheptacont", "Heptaheptacont",
			"Octaheptacont", "Nonaheptacont", "Octacont", "Henoctacont", "Dooctacont", "Trioctacont", "Tetraoctacont", "Pentaoctacont", "Hexaoctacont", "Heptaoctacont", "Octaoctacont",
			"Nonaoctacont", "Nonacont", "Hennonacont", "Dononacont", "Trinonacont", "Tetranonacont", "Pentanonacont", "Hexanonacont", "Heptanonacont", "Octanonacont", "Nonanonacont", "Hect" };
	private static String[] MULTIPLIERS = new String[] { "", "", "Di", "Tri", "Tetra", "Penta", "Hexa", "Hepta", "Octa", "Nona", "Deca", "Undeca" };
	private static String[] SECOND_LEVEL_MULTIPLIERS = new String[] { "", "", "Bis", "Tris", "Tetrakis", "Pentakis", "Hexakis", "Heptakis", "Octakis", "Nonakis", "Decakis", "Undecakis" };
	private static Map<String, String> COMMON_ALKYL_NAMES = new HashMap<String, String>() {
		{
			put("1,1-Dimethylpropyl", "tert-Pentyl");
			put("1,1-Dimethylethyl", "tert-Butyl");
			put("2,2-Dimethylpropyl", "Neopentyl");
			put("2-Methylpropyl", "Isobutyl");
			put("3-Methylbutyl", "Isopentyl");
			put("4-Methylpentyl", "Isohexyl");
			put("1-Methylpropyl", "sec-Butyl");
			put("1-Methylethyl", "Isopropyl");
		}
	};
	private static Map<String, String> NON_SYSTEMATIC_NAMES = new HashMap<String, String>() {
		{
			put("1-Ethene", "Ethylene");
			put("1,2-Propadiene", "Allene");
			put("1-Ethyne", "Acetylene");
		}
	};

	private static List<AtomsChain> extendChains(List<AtomsChain> chains, List<Atom> traversed) {
		List<AtomsChain> newChains = new ArrayList<AtomsChain>();
		for (AtomsChain currentChain : chains) {
			Atom currentNode = currentChain.lastAtom();
			traversed.add(currentNode);
			for (Atom neighbor : currentNode.getAdjacentAtoms()) {
				if (!traversed.contains(neighbor)) {
					AtomsChain longerChain = currentChain.copy();
					longerChain.add(neighbor);
					newChains.add(longerChain);
				}
			}
		}
		return newChains;
	}

	private static List<AtomsChain> longestFrom(Atom startNode, List<AtomsChain> longestChainsBefore, Atom offBoundsNode) {
		List<AtomsChain> longestChains = new ArrayList<AtomsChain>(longestChainsBefore), queue = new ArrayList<AtomsChain>();
		if (longestChains.size() == 0) {
			AtomsChain singleCarbonChain = new AtomsChain();
			singleCarbonChain.add(startNode);
			longestChains.add(singleCarbonChain);
		}
		AtomsChain seed = new AtomsChain();
		seed.add(startNode);
		queue.add(seed);
		List<Atom> traversed = new ArrayList<Atom>();
		traversed.add(offBoundsNode);
		while (!queue.isEmpty()) {
			List<AtomsChain> newChains = extendChains(queue, traversed);
			if (queue.get(0).getAtoms().size() > longestChains.get(0).getAtoms().size()) {
				longestChains.clear();
				longestChains.addAll(queue);
			}
			if (queue.get(0).getAtoms().size() == longestChains.get(0).getAtoms().size()) {
				longestChains.addAll(queue);
			}
			queue = newChains;
		}
		return longestChains;
	}

	private static List<Atom> branchCarbons(Atom trunkCarbon, Atom branchCarbon) {
		List<Atom> queue = new ArrayList<Atom>();
		List<Atom> traversed = new ArrayList<Atom>();
		traversed.add(trunkCarbon);
		queue.add(branchCarbon);
		while (!queue.isEmpty()) {
			Atom current = queue.remove(0);
			for (Atom adjacentAtom : current.getAdjacentAtoms()) {
				if (!traversed.contains(adjacentAtom)) {
					queue.add(adjacentAtom);
				}
			}
			traversed.add(current);
		}
		traversed.remove(trunkCarbon);
		return traversed;
	}

	private static String alkylGroupName(Atom trunkCarbon, Atom branchCarbon) {
		List<Atom> alkylGroup = branchCarbons(trunkCarbon, branchCarbon);
		AtomsChain longestChain = longestFrom(branchCarbon, new ArrayList<AtomsChain>(), trunkCarbon).get(0);
		if (alkylGroup.size() == longestChain.getAtoms().size()) {
			return IUPAC_NAMES[longestChain.getAtoms().size()] + "yl";
		}
		String prefix = sideChains(longestChain, trunkCarbon);
		String name = prefix + IUPAC_NAMES[longestChain.getAtoms().size()].toLowerCase() + "yl";
		String commonName = COMMON_ALKYL_NAMES.get(name);
		return (commonName != null) ? commonName : "(" + name + ")";
	}

	private static String commaSeparatedList(final List<Integer> list) {
		return list.toString().replaceAll("[ \\[\\]]", "");
	}

	private static List<Integer> numbering(AtomsChain chain) {
		List<Integer> numbering = new ArrayList<Integer>();
		int position = 0;
		for (Atom chainAtom : chain.getAtoms()) {
			position++;
			for (Atom adjacentAtom : chainAtom.getAdjacentAtoms()) {
				if (!chain.contains(adjacentAtom)) {
					numbering.add(position);
				}
			}
		}
		return numbering;
	}

	private static String sideChains(AtomsChain chain, Atom trunkCarbon) {
		Map<String, List<Integer>> sideChains = new HashMap<String, List<Integer>>();
		int position = 0;
		for (Atom chainAtom : chain.getAtoms()) {
			position++;
			for (Atom neighbor : chainAtom.getAdjacentAtoms()) {
				if (!neighbor.equals(trunkCarbon) && !chain.contains(neighbor)) {
					String alkyl = alkylGroupName(chainAtom, neighbor);
					if (!sideChains.containsKey(alkyl)) {
						sideChains.put(alkyl, new ArrayList<Integer>());
					}
					sideChains.get(alkyl).add(new Integer(position));
				}
			}
		}
//		START SORT HERE
// http://en.wikipedia.org/wiki/IUPAC_nomenclature_of_organic_chemistry
		List<String> sideChainsOrder = new ArrayList<String>(sideChains.keySet());
		Collections.sort(sideChainsOrder);
// End sort here
		String description = "";
		for (String alkyl : sideChainsOrder) {
			List<Integer> positions = sideChains.get(alkyl);
			String posList = commaSeparatedList(positions);
			String fullName;
			int multiples = positions.size();
			if (multiples > 1) {
				if (usesMultipliers(alkyl)) {
					fullName = SECOND_LEVEL_MULTIPLIERS[multiples] + alkyl.toLowerCase();
				} else {
					fullName = MULTIPLIERS[multiples] + alkyl.toLowerCase();
				}
			} else {
				fullName = alkyl;
			}
			if ("".equals(description)) {
				description += posList + "-" + fullName;
			} else {
				description += "-" + posList + "-" + fullName.toLowerCase();
			}
		}
		return description;
	}

	private static boolean usesMultipliers(String alkyl) {
		for (String multiplier : MULTIPLIERS) {
			if (alkyl.toLowerCase().indexOf("-" + multiplier.toLowerCase()) > -1) {
				return true;
			}
		}
		return false;
	}

	private static List<List<Integer>> sort(Set<List<Integer>> set) {
		List<List<Integer>> sorted = new ArrayList<List<Integer>>(set);
		Collections.sort(sorted, new Comparator<List<Integer>>() {
			public int compare(List<Integer> n1, List<Integer> n2) {
				if (n1.size() != n2.size()) {
					return n2.size() - n1.size();
				}
				for (int i = 0; i < n1.size(); i++) {
					if (n1.get(i).compareTo(n2.get(i)) != 0) {
						return n1.get(i) - n2.get(i);
					}
				}
				return 0;
			}
		});
		return sorted;
	}

	private static List<AtomsChain> getLongestChains(final List<AtomsChain> chains) {
		int longest = 0;
		for (AtomsChain chain : chains) {
			if (longest < chain.getAtoms().size()) {
				longest = chain.getAtoms().size();
			}
		}
		final int longestChainSize = longest;
		return new ArrayList<AtomsChain>() {
			{
				for (AtomsChain chain : chains) {
					if (chain.getAtoms().size() == longestChainSize) {
						add(chain);
					}
				}
			}
		};
	}

	private static AtomsChain highestValuedChain(final Molecule molecule) {
		Map<List<Integer>, AtomsChain> numberings = new HashMap<List<Integer>, AtomsChain>() {
			{
				for (AtomsChain chain : getLongestChains(molecule.getAllChains())) {
					put(numbering(chain), chain);
				}
			}
		};
		return numberings.get(sort(numberings.keySet()).get(0));
	}

	public static String buildFromMolecule(Molecule molecule) {
		AtomsChain bestChain = highestValuedChain(molecule);
		String prefix = sideChains(bestChain, null);
		String mainChain = IUPAC_NAMES[bestChain.getAtoms().size()];
		String name = prefix + (prefix.length() == 0 ? mainChain : mainChain.toLowerCase()) + "ane";
		return NON_SYSTEMATIC_NAMES.containsKey(name) ? NON_SYSTEMATIC_NAMES.get(name) : name;
	}
}
